import { isObject, isString, merge } from 'lodash-es'
import { doThen, mergeSourceMap } from '../utils'

export const mergeParallelHooks = (h1?: Function, h2?: Function): any => {
  if (h1 && h2) {
    return async function (this: any, ...args: any[]) {
      await Promise.all([h1.apply(this, args), h2.apply(this, args)])
    }
  } else {
    return h1 ?? h2
  }
}

export const mergeFirstHooks = (h1?: Function, h2?: Function): any => {
  if (h1 && h2) {
    return function (this: any, ...args: any[]) {
      return doThen(h1.apply(this, args), result => result ?? h2.apply(this, args))
    }
  } else {
    return h1 ?? h2
  }
}

export const mergeOptionsHooks = (h1?: Function, h2?: Function): any => {
  if (h1 && h2) {
    return async function (this: any, ...args: any[]) {
      const result1 = await h1.apply(this, args)
      const result2 = await h2.apply(this, args)

      return result1 && result2 ? merge(result1, result2) : result1 ?? result2
    }
  } else {
    return h1 ?? h2
  }
}

export const mergeSequentialHooks = (h1?: Function, h2?: Function): any => {
  if (h1 && h2) {
    return function (this: any, ...args: any[]) {
      return doThen(h1.apply(this, args), result => h2.apply(this, args))
    }
  } else {
    return h1 ?? h2
  }
}

export const mergeTransformHooks = (h1?: Function, h2?: Function): any => {
  if (h1 && h2) {
    return function (this: any, rawCode: string, ...args: any[]) {
      return doThen(h1.call(this, rawCode, ...args), result => {
        let isChange = false
        let code = rawCode,
          map: any,
          others: any,
          ast: any
        if (isString(result)) {
          isChange = true
          code = String(result)
        } else if (isObject(result) as boolean) {
          isChange = true
          ;({ code, map, ast, ...others } = result)
          code = code ?? ''
        }

        return doThen(h2.call(this, code, ...args), result => {
          let newCode = code,
            newMap: any,
            newAst = ast,
            newOthers: any
          if (isString(result)) {
            isChange = true
            newCode = String(result)
            newAst = undefined
          } else if (isObject(result) as boolean) {
            isChange = true
            ;({ code: newCode, map: newMap, ast: newAst, ...newOthers } = result)
            newCode = String(newCode ?? '')
          }

          if (!isChange) return

          return {
            ...others,
            ...newOthers,
            code: newCode,
            map: mergeSourceMap(map ?? { mappings: '' }, newMap ?? { mappings: '' }),
            ast: newAst
          }
        })
      })
    }
  } else {
    return h1 ?? h2
  }
}

export const mergeStringValueHooks = (h1: any, h2: any) => {
  if (h1 && h2) {
    if (typeof h1 === 'string' && typeof h2 === 'string') {
      return h2 + '\n' + h1
    }

    h1 = typeof h1 === 'function' ? h1 : () => h1
    h2 = typeof h2 === 'function' ? h2 : () => h2

    return function (this: any, ...args: any[]) {
      return doThen(h2.apply(this, args), result2 => {
        return doThen(h1.apply(this, args), result1 => {
          return result2 + '\n' + result1
        })
      })
    }
  } else {
    return h1 || h2
  }
}

export const mergeRollupPlugin = (plugin: import('rollup').Plugin, otherPlugin: Partial<import('rollup').Plugin>) => {
  const {
    buildStart,
    buildEnd,
    resolveId,
    load,
    transform,
    generateBundle,
    outputOptions,
    resolveFileUrl,
    resolveAssetUrl,
    options,
    resolveImportMeta,
    renderStart,
    renderError,
    renderChunk,
    renderDynamicImport,
    resolveDynamicImport,
    writeBundle,
    closeBundle,
    closeWatcher,
    watchChange,
    moduleParsed,
    shouldTransformCachedModule,
    augmentChunkHash,
    banner,
    footer,
    intro,
    outro
  } = plugin

  const {
    buildStart: other$buildStart,
    buildEnd: other$buildEnd,
    resolveId: other$resolveId,
    load: other$load,
    transform: other$transform,
    generateBundle: other$generateBundle,
    outputOptions: other$outputOptions,
    options: other$options,
    resolveFileUrl: other$resolveFileUrl,
    resolveAssetUrl: other$resolveAssetUrl,
    resolveImportMeta: other$resolveImportMeta,
    renderStart: other$renderStart,
    renderError: other$renderError,
    renderChunk: other$renderChunk,
    renderDynamicImport: other$renderDynamicImport,
    resolveDynamicImport: other$resolveDynamicImport,
    writeBundle: other$writeBundle,
    closeBundle: other$closeBundle,
    closeWatcher: other$closeWatcher,
    watchChange: other$watchChange,
    moduleParsed: other$moduleParsed,
    shouldTransformCachedModule: other$shouldTransformCachedModule,
    augmentChunkHash: other$augmentChunkHash,
    banner: other$banner,
    footer: other$footer,
    intro: other$intro,
    outro: other$outro
  } = otherPlugin

  plugin.buildStart = mergeParallelHooks(other$buildStart, buildStart)
  plugin.buildEnd = mergeParallelHooks(other$buildEnd, buildEnd)
  plugin.resolveId = mergeFirstHooks(other$resolveId, resolveId)
  plugin.load = mergeFirstHooks(other$load, load)
  plugin.transform = mergeTransformHooks(other$transform, transform)
  plugin.generateBundle = mergeSequentialHooks(other$generateBundle, generateBundle)
  plugin.outputOptions = mergeOptionsHooks(other$outputOptions, outputOptions)
  plugin.options = mergeOptionsHooks(other$options, options)
  plugin.resolveFileUrl = mergeFirstHooks(other$resolveFileUrl, resolveFileUrl)
  plugin.resolveAssetUrl = mergeFirstHooks(other$resolveAssetUrl, resolveAssetUrl)
  plugin.resolveImportMeta = mergeFirstHooks(other$resolveImportMeta, resolveImportMeta)
  plugin.renderDynamicImport = mergeFirstHooks(other$renderDynamicImport, renderDynamicImport)
  plugin.renderStart = mergeParallelHooks(other$renderStart, renderStart)
  plugin.renderError = mergeParallelHooks(other$renderError, renderError)
  plugin.renderChunk = mergeTransformHooks(other$renderChunk, renderChunk)
  plugin.resolveDynamicImport = mergeFirstHooks(other$resolveDynamicImport, resolveDynamicImport)
  plugin.writeBundle = mergeParallelHooks(other$writeBundle, writeBundle)
  plugin.closeBundle = mergeParallelHooks(other$closeBundle, closeBundle)
  plugin.closeWatcher = mergeParallelHooks(other$closeWatcher, closeWatcher)
  plugin.watchChange = mergeParallelHooks(other$watchChange, watchChange)
  plugin.moduleParsed = mergeParallelHooks(other$moduleParsed, moduleParsed)
  plugin.shouldTransformCachedModule = mergeFirstHooks(other$shouldTransformCachedModule, shouldTransformCachedModule)
  plugin.augmentChunkHash = mergeFirstHooks(other$augmentChunkHash, augmentChunkHash)
  plugin.banner = mergeStringValueHooks(other$banner, banner)
  plugin.footer = mergeStringValueHooks(other$footer, footer)
  plugin.intro = mergeStringValueHooks(other$intro, intro)
  plugin.outro = mergeStringValueHooks(other$outro, outro)
}
